使用 Eleventy 搭建静态博客

本文学自《Lesson 11: Blog feeds, tags and pagination | Learn Eleventy From Scratch》,介绍了如何使用 Eleventy 搭建一个个人静态博客。

博客内容位于 src/posts 下。内部包含 Markdown 博客文章,并且每篇文章中都包含元数据:

---
title: 'Why cross-cultural design really matters'
date: '2020-04-01'
tags: ['Culture', 'Design Thinking']
---

其中:制定了 date 日期。如果不指定的话,Eleventy 会采用文件在文件系统中的创建时间。除了 date 外,还可以取值包括:


创建 Collection

利用《Eleventy Collection》特性,将所有博客文章加入 blog 集合当中。在 .eleventy.js 中添加:

// Returns a collection of blog posts in reverse date order
config.addCollection('blog', collection => {
  return [...collection.getFilteredByGlob('./src/posts/*.md')].reverse();
});

创建博客列表

在《Eleventy Layout》目录中创建一个布局 feed.htmlfeed.njk):

{% extends "layouts/base.html" %}

{% set pageHeaderTitle = title %}
{% set pageHeaderSummary = content %}
{% set postListItems = pagination.items %}

{% block content %}
<article>
  {% include "partials/page-header.html" %} 
  {% include "partials/post-list.html" %}
</article>
{% endblock %}

其中:

通过变量声明,在 partial 中也能够访问到这些变量了。


创建 Partials

首先是 page-header.htmlpage-header.njk):

<div class="[ page-header ] [ bg-light-glare ]">
  <div class="[ wrapper ] [ flow ]">
    <h1 class="[ page-header__heading ] [ headline ]" data-highlight="primary">
      {{ pageHeaderTitle }}
    </h1>
    {% if pageHeaderSummary %}
    <div class="[ page-header__summary ] [ measure-long ]">
      {{ pageHeaderSummary | safe }}
    </div>
    {% endif %}
  </div>
</div>

接下来是文章列表 post-list.htmlpost-list.njk):

<div class="[ dot-shadow panel ] [ bg-secondary-glare ]" id="post-list">
  <div class="[ wrapper ] [ flow flow-space-700 ]">
    {% if postListHeadline %}
    <h2 class="[ headline ] [ measure-micro ]" data-highlight="primary">
      {{ postListHeadline }}
    </h2>
    {% endif %}
    <div>
      <ol class="[ post-list ] [ flow ]">
        {% for item in postListItems %}
        <li class="[ post-list__item ] [ leading-tight measure-long ]">
          <a href="{{ item.url }}" class="post-list__link">{{ item.data.title }}</a>
        </li>
        {% endfor %}
      </ol>
    </div>
  </div>
</div>

创建文章列表页数据

src 目录下创建一个 blog.md,它表示列表页的数据,根据该文件将会创建 blog.html,其内容主要包含元数据:

---
title: 'The Issue 33 Blog'
layout: 'layouts/feed.html'
pagination:
  data: collections.blog
  size: 5
permalink: 'blog{% if pagination.pageNumber > 0 %}/page/{{ pagination.pageNumber }}{% endif %}/index.html'
paginationPrevText: 'Newer posts'
paginationNextText: 'Older posts'
paginationAnchor: '#post-list'
---

The latest articles from around the studio, demonstrating our design
thinking, strategy and expertise.

其中:

对于 permalink,指定这段代码后,分页 url 路径为:

blog
blog/page/1
blog/page/2

如果不指定 permalink,默认的分页 url 路径为:

blog
blog/1
blog/2
Note

其中使用到了 Eleventy 的一个特性,允许我们在 Front Matter 中使用 Nunjucks


创建分页 Partials

前面能够生成分页的多页面,还需要在页面模板内打通连接。创建 pagination.htmlpagination.njk):

{# Only renders this section if there are links to render #}
{% if pagination.href.next or pagination.href.previous %}
  <footer class="[ pagination ] [ dot-shadow panel ] [ bg-light-glare font-sans weight-bold ]">
    <div class="wrapper">
      <nav class="pagination__inner" aria-label="Pagination links">
        {% if pagination.href.previous %}
          <a href="{{ pagination.href.previous }}{{ paginationAnchor }}" data-direction="backwards">
            <span>{{ paginationPrevText if paginationPrevText else 'Previous' }}</span>
          </a>
        {% endif %}
        {% if pagination.href.next %}
          <a href="{{ pagination.href.next }}{{ paginationAnchor }}" data-direction="forwards">
            <span>{{ paginationNextText if paginationNextText else 'Next' }}</span>
          </a>
        {% endif %}
      </nav>
    </div>
  </footer>
{% endif %}

创建 Tab 标签页

Eleventy 对标签有特殊的支持,会自动为 Front Matter 的 tag 创建《Eleventy Collection》。

src 文件夹中创建一个名为 tags.md 的新文件:

---
title: 'Tag Archive'
layout: 'layouts/feed.html'
pagination:
  data: collections
  size: 1
  alias: tag
  filter: ['all', 'nav', 'blog', 'work', 'featuredWork', 'people', 'rss']
permalink: '/tag/{{ tag | slug }}/'
---

这里直接取了所有 Collections 的数据,并且通过 filter 排除了一些专门功能类的 Collections,这样剩下的就都是 tags。

然后巧妙的设置分页个数为 1,这样每一页将是一个 Tag。

之后重写 feed.html,如果是 tag 页,不展示分页 Partials:

{% extends "layouts/base.html" %} 

{% set pageHeaderTitle = title %}
{% set pageHeaderSummary = content %}
{% set postListItems = pagination.items %}

{# If this is a tag, grab those items instead as one large collection #}
{% if tag %}
  {% set postListItems = collections[tag] %}
  {% set pageHeaderTitle = 'Blog posts filed under “' + tag + '”' %}
{% endif %}

{% block content %}
  <article>
    {% include "partials/page-header.html" %}
    {% include "partials/post-list.html" %}

    {# If we leave pagination in for tags, the next and prev links will
      link to tags and be rather confusing, so don't render in that situation #}
    {% if not tag %}
      {% include "partials/pagination.html" %}
    {% endif %}
  </article>

  {% include "partials/cta.html" %}
{% endblock %}

并且,在上述代码中:


博客详情页

本节继续课程《Lesson 12: Blog post view, directory data and filters | Learn Eleventy From Scratch》开始实现博客的文章详情页。在《Eleventy Layout》下创建 post.html

{% extends "layouts/base.html" %}

{% set pageHeaderTitle = title %}

{# Render post date and any tags that this post has been filed under #}
{% set pageHeaderSummary %}
  <time datetime="{{ date | w3DateFilter }}">{{ date | dateFilter }}</time>
  {% if tags %}
    <p class="visually-hidden" id="tags-desc">Tags that this post has been filed under.</p>
    <ul class="tags-list" aria-describedby="tags-desc">
      {% for tag in tags %}
        <li>
          <a href="/tag/{{ tag | slug }}/">#{{ tag | title | replace(' ', '') }}</a>
        </li>
      {% endfor %}
    </ul>
  {% endif %}
{% endset %}

{% block content %}
  <article>
    {% include "partials/page-header.html" %}
    
    <div class="[ page-content ] [ flow wrapper ] [ flow-space-700 gap-top-700 ]">
      {{ content | safe }}
    </div>
  </article>

  {% include "partials/cta.html" %}
{% endblock %}

其中:


使用 filter 处理时间

在上述详情的模板中,存在两个 filter:dateFilter 和 w3DateFilter,在本节中开发这两个 filter。创建一个 src/filters 目录,先创建 date-filter.js(注意文件名与 Filter 名的映射关系):

const moment = require('moment');

module.exports = value => {
  const dateObject = moment(value);
  return `${dateObject.format('Do')} of ${dateObject.format('MMMM YYYY')}`;
};

这里使用 moment 库对日期进行处理,将日期转换为 1st of April 2020 的格式。

注:安装依赖,npm install moment

第二个模板 w3-date-filter.js

module.exports = value => {
  const dateObject = new Date(value);

  return dateObject.toISOString();
};

最后,在 .eleventy.js 中添加注册 filter:

// Filters
const dateFilter = require('./src/filters/date-filter.js');
const w3DateFilter = require('./src/filters/w3-date-filter.js');

//...

// Add filters
config.addFilter('dateFilter', dateFilter);
config.addFilter('w3DateFilter', w3DateFilter);

批量设置模板

Eleventy 支持向目录中所有内容统一指定模板。比如,所有博客内容都位于 posts 下,创建 posts.json(与目录同名,表示统一配置):

{
  "layout": "layouts/post.html",
  "permalink": "/blog/{{ title | slug }}/index.html"
}

这种叫做目录数据文件。如果内容文件自身仍然指定了 layout,将会覆盖目录级别的通用设置。

其中,还指定了 permalink,默认情况下,Eleventy 将使用文件名及其目录来创建永久链接。这意味着,如果没有我们当前的设置,文件名为 my-lovely-post.md 的帖子将具有 /posts/my-lovely-post/index.html 的永久链接。不过,我们希望所有帖子都位于博客部分,因此我们在 permalink 的值中所做的是将 /blog 设置为根,然后从帖子的标题。


内容推荐

在本节中,将在详情页中添加更多博客内容推荐功能,会展示 3 个随即挑选的帖子。

eleventy-from-scratch/src/_data/helpers.js 中添加代码:

  /**
   * Filters out the passed item from the passed collection
   * and randomises and limits them based on flags
   *
   * @param {Array} collection The 11ty collection we want to take from
   * @param {Object} item The item we want to exclude (often current page)
   * @param {Number} limit=3 How many items we want back
   * @param {Boolean} random=true Wether or not this should be randomised
   * @returns {Array} The resulting collection
   */
getSiblingContent(collection, item, limit = 3, random = true) {
  let filteredItems = collection.filter(x => x.url !== item.url);

  if (random) {
    let counter = filteredItems.length;

    while (counter > 0) {
      // Pick a random index
      let index = Math.floor(Math.random() * counter);

      counter--;

      let temp = filteredItems[counter];

      // Swap the last element with the random one
      filteredItems[counter] = filteredItems[index];
      filteredItems[index] = temp;
    }
  }

  // Lastly, trim to length
  if (limit > 0) {
    filteredItems = filteredItems.slice(0, limit);
  }

  return filteredItems;
}

eleventy-from-scratch/src/_includes/layouts/post.html 中添加一个变量获取推荐内容:

{# Grab other posts that aren’t this one for the 'more from the blog' feed #}
{% set recommendedPosts = helpers.getSiblingContent(collections.blog, page) %}

之后,在相应位置添加:

{% if recommendedPosts %}
  <footer class="recommended-posts">
    {% set postListItems = recommendedPosts %}
    {% set postListHeadline = 'More from the blog' %}
    {% include "partials/post-list.html" %}
  </footer>
{% endif %}

本文作者:Maeiee

本文链接:使用 Eleventy 搭建静态博客

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!